'''Text Span object based on PDF raw dict extracted with ``PyMuPDF``.

Data structure for Span refer to
this `link <https://pymupdf.readthedocs.io/en/latest/textpage.html>`_::

    {
        # raw dict
        ---------------------------
        'bbox': (x0,y0,x1,y1),
        'color': sRGB
        'font': fontname,
        'size': fontsize,
        'flags': fontflags,
        'chars': [ chars ],

        # added dict
        ----------------------------
        'text': text,
        'style': [
            {
                'type': int,
                'color': int,
                'uri': str    # for hyperlink
            },
            ...
        ]
    }
'''
'''基于使用``PyMuPDF``提取的PDF原始字典的文本Span对象。

Span的数据结构请参考
这个`链接 <https://pymupdf.readthedocs.io/en/latest/textpage.html>`_::

    {
        # 原始字典
        ---------------------------
        'bbox': (x0,y0,x1,y1),
        'color': sRGB
        'font': fontname,
        'size': fontsize,
        'flags': fontflags,
        'chars': [ chars ],

        # 添加的字典
        ----------------------------
        'text': text,
        'style': [
            {
                'type': int,
                'color': int,
                'uri': str    # 用于超链接
            },
            ...
        ]
    }
'''

import fitz
from docx.shared import Pt, RGBColor
from docx.oxml.ns import qn
from .Char import Char
from ..common.Element import Element
from ..common.share import (RectType, rgb_value, rgb_component, decode)
from ..common import constants
from ..common import docx
from ..shape.Shape import Shape


class TextSpan(Element):
    '''Object representing text span.'''
    '''表示文本span的对象。'''
    def __init__(self, raw:dict=None):
        raw = raw or {}
        self.color = raw.get('color', 0)
        self.flags = raw.get('flags', 0)

        # filter empty chars
        # 过滤空字符
        chars = [Char(c) for c in raw.get('chars', [])] # type: list[Char]
        # ignore replacement character, see issue#256
        # 忽略替换字符，参见issue#256
        self.chars = [char for char in chars if char.c not in ('', '\ufffd')]
        self._text = raw.get('text', '') # not an original key from PyMuPDF
                                         # 不是PyMuPDF中的原始键

        # font metrics
        # line_height is the standard single line height used in relative line spacing,
        # while exact line spacing is used when line_height==-1 by default.
        # 字体度量
        # line_height是相对行距中使用的标准单行高度，
        # 当line_height==-1时，默认使用精确行距。
        self.font = decode(raw.get('font', '')) # in case unicode in font name
                                                # 防止字体名称中有unicode字符
        self.size = raw.get('size', 12.0)
        self.ascender = raw.get('ascender', 1.0)
        self.descender = raw.get('descender', 0.0)
        self.line_height = raw.get('line_height', -1)  # not an original key
                                                      # 不是原始键

        # introduced attributes
        # a list of dict: { 'type': int, 'color': int }
        # 引入的属性
        # 字典列表：{ 'type': int, 'color': int }
        self.style = raw.get('style', [])

        # char spacing between adjacent two chars -> pdf operador Tc
        # positive to expand space, otherwise condense
        # just an attribute placeholder: not used yet
        # 相邻两个字符之间的字符间距 -> pdf操作符Tc
        # 正值扩大间距，否则缩小间距
        # 仅作为属性占位符：尚未使用
        self.char_spacing = raw.get('char_spacing', 0.0)

        # init text span element
        # 初始化文本span元素
        super().__init__(raw)

        # in rare case, the font is unamed, so change font and update bbox accordingly
        # 在罕见情况下，字体未命名，因此更改字体并相应地更新bbox
        if self.chars and 'UNNAMED' in self.font.upper():
            self._change_font_and_update_bbox(constants.DEFAULT_FONT_NAME)


    @property
    def text(self):
        '''Get span text. Note joining chars is in a higher priority.'''
        '''获取span文本。注意连接字符具有更高的优先级。'''
        return ''.join([char.c for char in self.chars]) if self.chars else self._text

    @text.setter
    def text(self, value):
        '''Set span text directly in case no chars are stores, e.g. restored from json.'''
        '''在没有存储字符的情况下直接设置span文本，例如，从json还原。'''
        self._text = value

    def cal_bbox(self):
        '''Calculate bbox based on contained instances.'''
        '''根据包含的实例计算bbox。'''
        bbox = fitz.Rect()
        for char in self.chars: bbox |= char.bbox
        return bbox

    @property
    def is_valid_line_height(self): return self.line_height!=-1


    def _change_font_and_update_bbox(self, font_name:str):
        '''Set new font, and update font size, span/char bbox accordingly.

        It's generally used for span with unnamed fonts.
        See this `issue <https://github.com/pymupdf/PyMuPDF/issues/642>`_.

        In corner case, where the PDF file containing unnamed and not embedded fonts, the span bbox
        extracted from ``PyMuPDF`` is not correct. ``PyMuPDF`` provides feature to replace these
        unnamed fonts with specified fonts, then extract correct bbox from the updated PDF. Since we
        care less about the original PDF itself but its layout, the idea here is to set a default
        font for text spans with unnamed fonts, and estimate the updated bbox with method from
        ``fitz.TextWriter``.

        Args:
            font_name (str): Font name.
        '''
        '''设置新字体，并相应地更新字体大小、span/char的bbox。

        它通常用于具有未命名字体的span。
        参见此`问题 <https://github.com/pymupdf/PyMuPDF/issues/642>`_。

        在特殊情况下，PDF文件包含未命名且未嵌入的字体，从``PyMuPDF``提取的span bbox
        是不正确的。``PyMuPDF``提供了一个功能，可以用指定的字体替换这些
        未命名的字体，然后从更新的PDF中提取正确的bbox。由于我们
        关注的不是原始PDF本身，而是它的布局，这里的想法是为具有未命名字体的文本span
        设置默认字体，并使用``fitz.TextWriter``的方法估计更新的bbox。

        参数:
            font_name (str): 字体名称。
        '''
        # set new font property
        # 设置新字体属性
        self.font = font_name

        # compute text length under new font with that size
        # 计算在该大小的新字体下的文本长度
        font = fitz.Font(font_name)
        new_length = font.text_length(self.text, fontsize=self.size)
        if new_length > self.bbox.width:
            self.size *= self.bbox.width / new_length

        # estimate occupied rect when added with TextWriter
        # 估计使用TextWriter添加时占用的矩形区域
        x0, y0, x1, y1 = self.bbox
        tw = fitz.TextWriter((0, 0, x1, y1))
        rect, _ = tw.append(
            self.chars[0].origin, # the bottom left point of the first character
                                 # 第一个字符的左下角点
            self.text,
            font=font,
            fontsize=self.size
        )

        # update span bbox
        # - x-direction: use original horizontal range
        # - y-direction: centerline defined by estimated vertical range, and height by font size
        # 更新span bbox
        # - x方向：使用原始水平范围
        # - y方向：由估计的垂直范围定义中心线，高度由字体大小决定
        buff = (rect.height-self.size)/2.0
        y0 = rect.y0 + buff
        y1 = rect.y1 - buff
        self.update_bbox((x0, y0, x1, y1))

        # update contained char bbox
        # 更新包含的字符bbox
        for char in self.chars:
            x0, _, x1, _ = char.bbox
            char.update_bbox((x0, y0, x1, y1))


    def add(self, char:Char):
        '''Add char and update bbox accordingly.'''
        '''添加字符并相应地更新bbox。'''
        self.chars.append(char)
        self.union_bbox(char)


    def lstrip(self):
        '''Remove blanks at the left side, but keep one blank.'''
        '''删除左侧的空格，但保留一个空格。'''
        original_text = self.text
        if not original_text.startswith(' '*2): return False

        # keep one blank
        # 保留一个空格
        num_blanks = len(original_text) - len(original_text.lstrip())
        self.chars = self.chars[num_blanks-1:]
        self.update_bbox(rect=self.cal_bbox())
        return True


    def rstrip(self):
        '''Remove blanks at the right side, but keep one blank.'''
        '''删除右侧的空格，但保留一个空格。'''
        original_text = self.text
        if not original_text.endswith(' '*2): return False

        # keep one blank
        # 保留一个空格
        num_blanks = len(original_text) - len(original_text.rstrip())
        self.chars = self.chars[:1-num_blanks]
        self.update_bbox(rect=self.cal_bbox())
        return True


    def store(self):
        res = super().store()
        res.update({
            'color': self.color,
            'font': self.font,
            'size': self.size,
            'line_height': self.line_height, 
            'flags': self.flags,
            'text': self.text,
            'style': self.style,
            'char_spacing': self.char_spacing
        }) # not storing chars for space saving
           # 为节省空间不存储chars
        return res


    def plot(self, page, color:tuple): super().plot(page, stroke=color, fill=color, width=0)


    def split(self, rect:Shape, horizontal:bool=True):
        """Split span with the intersection: span-intersection-span.

        Args:
            rect (Shape): Target shape to split this text span.
            horizontal (bool, optional): Text direction. Defaults to True.

        Returns:
            list: Split text spans.
        """
        """使用交集分割span：span-交集-span。

        参数:
            rect (Shape): 用于分割此文本span的目标形状。
            horizontal (bool, optional): 文本方向。默认为True。

        返回:
            list: 分割后的文本spans。
        """
        # any intersection in this span?
        # NOTE: didn't consider the case that an underline is out of a span
        # 此span中有任何交集吗？
        # 注意：没有考虑下划线超出span的情况
        intsec = rect.bbox & self.bbox

        # no, then add this span as it is
        # Note the case bool(intsec)=True but intsec.get_area()=0
        # 没有，则按原样添加此span
        # 注意这种情况：bool(intsec)=True但intsec.get_area()=0
        if intsec.is_empty: return [self]

        # yes, then split spans:
        # - add new style to the intersection part
        # - keep the original style for the rest
        # 是的，然后分割spans：
        # - 为交集部分添加新样式
        # - 为其余部分保留原始样式
        split_spans = [] # type: list[TextSpan]

        # expand the intersection area, e.g. for strike through line,
        # the intersection is a `line`, i.e. a rectangle with very small height,
        # so expand the height direction to span height
        # 扩展交集区域，例如，对于删除线，
        # 交集是一条"线"，即一个高度很小的矩形，
        # 因此将高度方向扩展到span高度
        if horizontal:
            intsec.y0 = self.bbox.y0
            intsec.y1 = self.bbox.y1
        else:
            intsec.x0 = self.bbox.x0
            intsec.x1 = self.bbox.x1

        # calculate chars in the format rectangle
        # combine an index with enumerate(), so the second element is the char
        # 计算格式矩形中的字符
        # 结合enumerate()的索引，因此第二个元素是字符
        f = lambda items: items[1].contained_in_rect(rect, horizontal)
        index_chars = list(filter(f, enumerate(self.chars)))

        # then we get target chars in a sequence
        # 然后我们获得目标字符序列
        pos = index_chars[0][0] if index_chars else -1 # start index -1 if nothing found
                                                       # 如果没有找到，起始索引为-1
        length = len(index_chars)
        pos_end = max(pos+length, 0) # max() is used in case: pos=-1, length=0
                                     # max()用于处理以下情况：pos=-1, length=0

        # split span with the intersection: span-intersection-span
        # left part if exists
        # 使用交集分割span：span-交集-span
        # 左侧部分（如果存在）
        if pos > 0:
            if horizontal:
                bbox = (self.bbox.x0, self.bbox.y0, intsec.x0, self.bbox.y1)
            else:
                bbox = (self.bbox.x0, intsec.y1, self.bbox.x1, self.bbox.y1)
            split_span = self.copy().update_bbox(bbox)
            split_span.chars = self.chars[0:pos]
            split_spans.append(split_span)

        # middle intersection part if exists
        # 中间交集部分（如果存在）
        if length > 0:
            bbox = (intsec.x0, intsec.y0, intsec.x1, intsec.y1)
            split_span = self.copy().update_bbox(bbox)
            split_span.chars = self.chars[pos:pos_end]            
            split_span._parse_text_format(rect, horizontal)  # update style
                                                           # 更新样式
            split_spans.append(split_span)

        # right part if exists
        # 右侧部分（如果存在）
        if pos_end < len(self.chars):
            if horizontal:
                bbox = (intsec.x1, self.bbox.y0, self.bbox.x1, self.bbox.y1)
            else:
                bbox = (self.bbox.x0, self.bbox.y0, self.bbox.x1, intsec.y0)
            split_span = self.copy().update_bbox(bbox)
            split_span.chars = self.chars[pos_end:]
            split_spans.append(split_span)

        return split_spans


    def _parse_text_format(self, rect:Shape, horizontal:bool=True):
        """Parse text style based on the position to a rect shape.

        Args:
            rect (Shape): Target rect shape representing potential text style.
            horizontal (bool, optional): Horizontal text direction. Defaults to True.

        Returns:
            bool: Parsed text style successfully or not.
        """
        """根据与矩形形状的位置关系解析文本样式。

        参数：
            rect (Shape)：表示潜在文本样式的目标矩形形状。
            horizontal (bool, optional)：水平文本方向。默认为True。

        返回：
            bool：是否成功解析文本样式。
        """

        # Skip table border/shading
        # 跳过表格边框/阴影
        if rect.equal_to_type(RectType.BORDER) or rect.equal_to_type(RectType.SHADING):
            return False

        # set hyperlink
        # 设置超链接
        if rect.equal_to_type(RectType.HYPERLINK):
            self.style.append({
                'type': rect.type,
                'color': rect.color,
                'uri': rect.uri
            })
            return True

        # considering text direction
        # 考虑文本方向
        idx = 1 if horizontal else 0

        # recognize text format based on rect and the span it applying to
        # region height
        # 根据矩形和它应用到的span识别文本格式
        # 区域高度
        h_rect = rect.bbox[idx+2] - rect.bbox[idx]
        h_span = self.bbox[idx+2] - self.bbox[idx]

        # distance to span bottom border
        # 到span底部边界的距离
        d = abs(self.bbox[idx+2] - rect.bbox[idx])

        # highlight: both the rect height and overlap must be large enough
        # 高亮：矩形高度和重叠部分都必须足够大
        if h_rect >= 0.5*h_span:
            # In general, highlight color isn't white
            # 通常，高亮颜色不是白色
            if rect.color != rgb_value((1,1,1)) and \
                self.get_main_bbox(rect, constants.FACTOR_MAJOR):
                rect.type = RectType.HIGHLIGHT

        # near to bottom of span? yes, underline
        # 接近span底部？是的，下划线
        elif d <= 0.25*h_span:
            rect.type = RectType.UNDERLINE

        # near to center of span? yes, strike-through-line
        # 接近span中心？是的，删除线
        elif 0.35*h_span < d < 0.75*h_span:
            rect.type = RectType.STRIKE

        # check rect type again
        # 再次检查矩形类型
        if not rect.is_determined: return False

        style =  {
            'type': rect.type,
            'color': rect.color
        }
        self.style.append(style)

        return True


    def intersects(self, rect):
        '''Create new TextSpan object with chars contained in given bbox.

        Args:
            rect (fitz.Rect): Target bbox.
        '''
        '''创建一个新的TextSpan对象，其中包含给定bbox中包含的字符。

        参数：
            rect (fitz.Rect)：目标bbox。
        '''
        # add span directly if fully contained in bbox
        # 如果完全包含在bbox中，则直接添加span
        if rect.contains(self.bbox):
            return self.copy()

        # no intersection
        # 没有交集
        if not rect.intersects(self.bbox):
            return TextSpan()

        # further check chars in span
        # 进一步检查span中的字符
        span = self.copy()
        span.chars.clear()
        span.update_bbox((0.0,0.0,0.0,0.0))

        for char in self.chars:
            if char.get_main_bbox(rect, constants.FACTOR_A_HALF): # contains at least a half part
                                                                 # 至少包含一半部分
                span.chars.append(char)
                span.union_bbox(char)

        return span


    def make_docx(self, paragraph):
        '''Add text span to a docx paragraph, and set text style, e.g.
        font, color, underline, hyperlink, etc.

        .. note::
            Hyperlink and its style is parsed separately from pdf. For instance, regarding a general
            hyperlink with an underline, the text and uri is parsed as hyperlink itself, while the
            underline is treated as a normal text style.
        '''
        '''将文本span添加到docx段落，并设置文本样式，例如
        字体、颜色、下划线、超链接等。

        .. 注意::
            超链接及其样式是从pdf中单独解析的。例如，对于一个带有下划线的一般
            超链接，文本和uri被解析为超链接本身，而
            下划线被视为普通文本样式。
        '''
        # Create hyperlink in particular, otherwise add a run directly
        # 特别创建超链接，否则直接添加一个run
        for style in self.style:
            if style['type']==RectType.HYPERLINK.value and self.text.strip():
                docx_run = docx.add_hyperlink(paragraph, style['uri'], self.text)
                break
        else:
            docx_run = paragraph.add_run(self.text)

        # set text style, e.g. font, underline and highlight
        # 设置文本样式，例如字体、下划线和高亮
        self._set_text_format(docx_run)

        # set charters spacing
        # 设置字符间距
        if self.char_spacing:
            docx.set_char_spacing(docx_run, self.char_spacing)


    def _set_text_format(self, docx_run):
        '''Set text format for ``python-docx.run`` object.'''
        '''为``python-docx.run``对象设置文本格式。'''
        # set style
        # https://python-docx.readthedocs.io/en/latest/api/text.html#docx.text.run.Font
        # 设置样式
        # https://python-docx.readthedocs.io/en/latest/api/text.html#docx.text.run.Font

        # basic font style
        # line['flags'] is an integer, encoding bool of font properties:
        # bit 0: superscripted (2^0)
        # bit 1: italic (2^1)
        # bit 2: serifed (2^2)
        # bit 3: monospaced (2^3)
        # bit 4: bold (2^4)
        # 基本字体样式
        # line['flags']是一个整数，编码字体属性的布尔值：
        # 位0：上标(2^0)
        # 位1：斜体(2^1)
        # 位2：衬线(2^2)
        # 位3：等宽(2^3)
        # 位4：粗体(2^4)
        docx_run.superscript = bool(self.flags & 2**0)
        docx_run.italic = bool(self.flags & 2**1)
        docx_run.bold = bool(self.flags & 2**4)

        # font name
        # 字体名称
        font_name = self.font
        docx_run.font.name = font_name
        docx_run._element.rPr.rFonts.set(qn('w:eastAsia'), font_name) # for CJK characters
                                                                      # 用于CJK字符
        docx_run.font.color.rgb = RGBColor(*rgb_component(self.color))

        # font size
        # NOTE: only x.0 and x.5 is accepted in docx, so set character scaling accordingly
        # if the font size doesn't meet this condition.
        # 字体大小
        # 注意：docx中只接受x.0和x.5，所以相应地设置字符缩放
        # 如果字体大小不满足这个条件。
        font_size = round(self.size*2)/2.0
        docx_run.font.size = Pt(font_size)

        # adjust by set scaling
        # 通过设置缩放进行调整
        scale = self.size / (font_size or self.size or 1)
        if abs(scale-1.0)>=0.01:
            docx.set_char_scaling(docx_run, scale)

        # font style parsed from PDF rectangles:
        # e.g. highlight, underline, strike-through-line
        # 从PDF矩形解析的字体样式：
        # 例如高亮、下划线、删除线
        for style in self.style:

            t = style['type']
            # Built-in method is provided to set highlight in python-docx,but supports only
            # limited colors; so, set character shading instead if out of highlight color scope.
            # python-docx提供了内置方法来设置高亮，但只支持
            # 有限的颜色；因此，如果超出高亮颜色范围，则设置字符阴影。
            if t==RectType.HIGHLIGHT.value:
                docx.set_char_shading(docx_run, style['color'])

            # underline set with built-in method `font.underline` has a same color with text.
            # so, try to set a different color with xml if necessary
            # 使用内置方法`font.underline`设置的下划线与文本颜色相同。
            # 因此，如有必要，尝试使用xml设置不同的颜色
            elif t==RectType.UNDERLINE.value:
                if self.color==style['color']:
                    docx_run.font.underline = True
                else:
                    docx.set_char_underline(docx_run, style['color'])

            # same color with text for strike line
            # 删除线与文本颜色相同
            elif t==RectType.STRIKE.value:
                docx_run.font.strike = True
